查看原文
其他

【老万】我在谷歌弄啥咧(十六):造泵记

老万 老万故事会
2024-08-23

昨天我提到从谷歌学到的最重要技能之一是从第一性原理(first principles)出发思考。

结果这个名词把很多朋友搞懵了。这个要怪我没说清楚,今天就再掰扯掰扯。

所谓第一性原理思维,就是从最基本的原理出发去分析,抓住问题的本质,也就是本质的问题,不接受任何预设的结论。

说人话,就是自己从头推公式,不抄作业。

我昨天还举了一个设计谷歌 C++ mocking 框架 Google Mock(又称gMock)的例子,说到 gMock 是一个嵌入在 C++ 中的DSL(特定领域语言)。

好玩的是早期的 gMock 其实包含了两个 DSL:一个在系统的表面(也就是 gMock 的 API),大家都看得见。另一个藏在 gMock 的实现里面,只有 gMock 的维护者需要了解。

为毛要在一个框架中塞入多达二个 DSL 呢?到底是中二症还是力比多用不完综合症?

要回答这个问题,咱们还得回到昨天的主题上来:大无畏精神和第一性原理思维。

我是从 2006 年开始搞 gMock 的。那时候,天苍苍,野茫茫,《老万故事会》还没有诞生,《老万故事会》的读者还有很多没有出世,汪峰们还在街上在桥下在田野中唱着那无人问津的歌谣。最重要的是:那时候的 C++ 跟今天比是一种截然不同的语言,不能说是捉襟见肘吧,起码也是一穷二白。

这就给攒 gMock 带来了巨大的挑战。毕竟,它需要利用最高级的 C++ 宏和模版特性来克服 C++ 的静态类型和缺乏反射(reflection)的问题。

我们知道,函数的参数个数有多有少:零元函数不需要参数,五毛函数需要五毛钱参数,一元函数需要一个参数,二元函数需要两个参数,依此类推。参数的个数被称为元数(arity)

gMock 的用户需要 mock 不同元数的函数。为了支持这些需求,gMock 要根据函数的元数提供不同的实现:零元函数有一个实现,一元函数又有一个实现,等等等等。

实现这些功能,最好的办法当然是用上可变参数模板(variadic templates)和可变参数宏(variadic macros)。

然鹅问题来了,多年前的 C++ 并没有可变参数模板这样的高级功能,连可变参数宏也不好使。

最高级的食材,只需要最简单的烹饪。但要是没有高级食材,厨师就只好另辟蹊径。比如,将饭蒸熟后,加入一倍的水,再蒸一次,称为双蒸饭,可以暂时撑饱肚子。又比如,用人尿培养小球藻再让人吃下去,可以治饿痨病。

于是,gMock 的实现代码中被迫出现大量重复。我们可以选择负重前行,手动编写这些代码,但这样得到的只能是一个无法维护的系统:

  1. 写这种重复代码完全是亵渎程序员,没有一个有尊严的程序员会想维护 gMock。当然,我们有很多程序员是没有尊严的,所以这一点也不是什么大问题。

  2. 然而,这些不同元数的实现不是简单的拷贝复制,而是略有不同。复制时很容易犯错误啊啊啊。

  3. 这种方法对系统能支持的元数有人为的上限。要是想支持更多参数的函数,就必须重复更多的代码 — 这就不好玩了。

  4. 要是发现 gMock 有一个 bug,必须在多个地方修正。这种苦活既繁琐又容易出错。


聪明人可能已经想到了:干嘛自己重复写代码,写一个代码生成器不就好了吗。写完了,跑一遍,代码就生出来了。要是发现出来的代码有 bug,那就改改生成器,再跑一遍。一遍不行,就改两遍。

咋说呢,这种方法吧,不是不行,但也不是很行:

这个“代码生成器”听起来就很高大上,实际做起来确实不好维护。毕竟,C++ 和 Python 这样的通用编程语言不是专门为这一类任务设计的。如果写这么一个生成器,要改 gMock 的实现就得费老鼻子劲了。

这种半吊子方案怎么能行呢?一定还有更好的招!

我想,既然用通用语言写这个生成器不顺手,那就来个 DSL 吧。

于是有了 Pump(泵),一个用于编写可变元(variadic)C++ 代码的迷你 DSL。

Pump 这个名字是一个递归缩写:它代表 Pump is Useful for Meta Programming(Pump 对元编程有用)。它也可以代表 Pretty Useful for Meta Programming(对元编程嘿有用),或者代表 Practical Utility for Meta Programming(元编程实用工具)。

三个代表,随你心情而定,总有一款适合你。

取这个名字还有一层含义:“pump”是一个通过不断重复完成任务的动作,比如给气球打气也叫 pump。

是滴,程序员喜欢开这种傻乎乎的玩笑,乐此不疲。我就是一个典型的程序员。

由于 Pump 是专门为编写可变元 C++ 代码量身定做的,C++ 程序员学起来很简单。你可以把普通的 C++ 代码跟 Pump 特有的功能混合在一起使用。

例如,Pump 用 $ 字符开始一个 Pump 关键词,用 $$ 开始一个元注释(就是在 Pump 程序中的注释,不会出现在生成的代码中),用 [[ 和 ]] 将代码分块处理。

我们来看一个具体的 Pump 代码示例:

$var n = 3 $$ 定义一个元变量 n。$range i 0..n $$ 声明元迭代器 i 的范围。$for i [[ $$ 元循环。// Foo$i 对$i元谓词执行blah操作。$range j 1..itemplate <size_t N $for j [[, typename A$j]]>class Foo$i {$if i == 0 [[ blah a;]] $elif i <= 2 [[ blah b;]] $else [[ blah c;]]};
]]

这段代码经 Pump 翻译后,就成了这样的 C++ 代码:

// Foo0 对0元谓词执行blah操作。template <size_t N>class Foo0 { blah a;};
// Foo1 对1元谓词执行blah操作。template <size_t N, typename A1>class Foo1 { blah b;};
// Foo2 对2元谓词执行blah操作。template <size_t N, typename A1, typename A2>class Foo2 { blah b;};
// Foo3 对3元谓词执行blah操作。template <size_t N, typename A1, typename A2, typename A3>class Foo3 { blah c;};

熟悉模板语言(templating languages)的朋友可能已经发现了:Pump 就是一个模板语言而已。

有了 Pump,gMock 中的可变元代码编写和维护就简单了。

总之,在面对 gMock 的实现需要大量重复代码的问题时,我发扬大无畏精神,没有因这个问题难解决而放弃。相反,我从第一性原理出发(想清楚 gMock 维护者到底需要什么样的工具),找到了一个办法让维护者自然地表达他们的意图,摒弃了让他们自己搞定代码生成器这种不负责任的想法。

时代的变迁总是会抹去历史的痕迹。随着 C++ 编译器对可变参数宏和可变参数模板的支持慢慢到位,Pump 也渐渐失去了存在的意义,最终迷失在黑夜里被 gMock 放弃了。今天的 gMock 已经完全不用 Pump 了。不过,互联网是有记忆的,你还是能在网上找到我为 Pump 编写的原始文档:https://github.com/google/googletest/blob/release-1.8.0/googletest/docs/PumpManual.md

~~~~


猜你会喜欢《我在谷歌弄啥咧》系列:



~~~~


关注老万故事会公众号:


本公众号不开赞赏不放广告。如果喜欢这篇文章,点个在看,转发给朋友就是对老万的最大支持。谢谢大家🙏

继续滑动看下一个
老万故事会
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存